/*
Copyright 2010 Peter Krajnc, SCREENWORKER

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.screenworker.compactmvc.core
{
	import com.screenworker.compactmvc.interfaces.IModel;
	import com.screenworker.compactmvc.patterns.Controller;
	import com.screenworker.compactmvc.vo.NoteVO;
	
	/**
	 * MVC Core
	 * @author Peter Krajnc
	 * 
	 */	
	public class CompactMVC
	{
		public static const VERSION: String 			= "0.1.3";
		public static const AUTHOR:String 				= "Peter Krajnc, SCREENWORKER";
		public static const SOURCE:String 				= "http://compactmvc.googlecode.com/svn/trunk/";
		public static const EXAMPLE_FLEX:String 		= "http://compactmvc-example-flex.googlecode.com/svn/trunk/";
		public static const EXAMPLE_FLASH:String 		= "http://compactmvc-example-flash.googlecode.com/svn/trunk/";
		public static const EXAMPLE_AIR:String 			= "http://compactmvc-example-air.googlecode.com/svn/trunk/";
		
		//Log Level
		public static const LOGLEVEL_ALL:String 		= 'all';
		public static const LOGLEVEL_DEBUG:String 		= 'debug';
		public static const LOGLEVEL_NONE:String 		= 'none';
		
		public static const METATAG_COMPACT_WIRE:String	= 'CompactWire';
		
		//Singleton Instance
		private static var instance:CompactMVC;
		private var _core:Core;

		/**
		* Static accessor method.
		* @return A unique instance of this class.
		*/
		public static function getInstance():CompactMVC 
		{	
			if(!instance)
			{
				instance = new CompactMVC( new SingletonEnforcer() );
			}
			return instance;
			
		}
		
		/**
		 * constructor 
		 * @param enforcer Singleton enforcer
		 * 
		 */		
		public function CompactMVC(enforcer:SingletonEnforcer) 
		{
			if (!enforcer)
			{
				throw new Error('CompactMVC must be instantiated through CompactMVC.getInstance()');
			}
			_core = new Core();	
		}
		
      	/**
      	 * registers the view instance
      	 * @param controllerName
      	 * @param viewName
      	 * @param viewInstance
      	 * 
      	 */		
      	public function registerView(controllerName:String, viewName:String, viewInstance:Object):void
      	{    		
      		core.registerView(controllerName, viewName, viewInstance);
      	}
      	
      	/**
      	 * remove the registered view instance
      	 * @param controllerName
      	 * @param viewName
      	 * @param viewInstance
      	 * 
      	 */		
      	public function removeView(controllerName:String, viewName:String):void
      	{    		
      		core.removeView(controllerName, viewName);
      	}
      	
		/**
		 * notifies one controller by name 
		 * @param controllerName
		 * @param notificationType
		 * @param data
		 * 
		 */      	
		public function sendNotificationTo(controllerName:String, notificationType:String, data:Object=null, delay:Number=0):void 
		{	
			core.retrieveController(controllerName).sendNotification( new NoteVO(notificationType, data), delay );
		}
		
		/**
		 * notify all controllers
		 * @param notificationType
		 * @param data
		 * 
		 */		
		public function sendJunctionNotification(notificationType:String, data:Object=null, delay:Number=0):void
		{
			var controllers:Array = core.retrieveRegisteredControllers();
			for each(var item:Controller in controllers)
			{
				item.sendNotification( new NoteVO(notificationType, data), delay );
				trace("sendJunctionNotification:" + item.getName() + " type:" + notificationType);
			}
		}
		
		/**
		 * returns the core Instance 
		 * @return 
		 * 
		 */	
		public function get core():Core
		{
			return _core;
		}
		
	}
}

import com.screenworker.compactmvc.interfaces.IController;
import com.screenworker.compactmvc.interfaces.ICommand;
import com.screenworker.compactmvc.interfaces.IModel;
import com.screenworker.compactmvc.vo.ControllerVO;
import com.screenworker.compactmvc.patterns.Controller;
import com.screenworker.compactmvc.patterns.Model;
import com.screenworker.compactmvc.vo.NoteVO;
import com.screenworker.compactmvc.core.CompactMVC;
import flash.utils.getQualifiedClassName;
import flash.net.getClassByAlias;
import flash.utils.describeType;

/**
 * The CompactMVC Core
 * @author Peter Krajnc, Screenworker
 * 
 */	
class Core
{
	private var _controller:Array = new Array();
	/**
	 * constructor 
	 * @param enforcer
	 * 
	 */		
	public function Core()
	{	
		
	}
	
	/**
	 * registered a controller 
	 * @param controller
	 * 
	 */		
	public function registerController(controller:Controller):void
	{
		if( isControllerRegistered(controller.getName()) )
		{
			throw new Error('controller already registered: ' + controller.getName());
		}  
		
		 _controller[ controller.getName() ] = new ControllerVO(controller);
		 controller.registerHandler();
	}
	
	/**
	 * removes a controller 
	 * @param controller
	 * 
	 */		
	public function removeController(controller:Controller):void
	{
		if( !isControllerRegistered(controller.getName()) )
		{
        	throw new Error('controller not registered: ' + controller.getName());
		}  
		_controller[  controller.getName()  ] = null;
		delete _controller[  controller.getName()  ];
  	}
  	
  	/**
  	 * retrieves a registered Controller by Name
  	 * @param controllerName
  	 * @return 
  	 * 
  	 */      	
  	public function retrieveController(controllerName:String):Controller
	{	
		if( !isControllerRegistered(controllerName) )
		{
			log(controllerName, 'controller not registered: ' + controllerName);
			return null;
		}
		return ControllerVO(_controller[ controllerName ]).controller;
	}
	
	/**
  	 * retrieves all registered Controllers 
  	 * @return 
  	 * 
  	 */      	
  	public function retrieveRegisteredControllers():Array
	{	
		var arr:Array = new Array();
		for each(var item:ControllerVO in _controller)
		{
			arr.push(item.controller);
		}
		return arr;
	}
	
	
	public function registerCommand(controller:Controller, notificationType:String, commandClassRef:Class):void
	{
		if(ControllerVO(_controller[ controller.getName() ]).commands[ notificationType ] != null )
		{
			throw new Error('command already registered: ' + notificationType);
		}  
		ControllerVO(_controller[ controller.getName() ]).commands[notificationType] = commandClassRef;
	}
	
	/**
	 * removes a registered Command 
	 * @param controller
	 * @param notificationType
	 * 
	 */		
	public function removeCommand(controller:Controller, notificationType:String):void
	{
		if( ControllerVO(_controller[ controller.getName() ]).commands[notificationType] == null)
		{
        	throw new Error('command not registered: ' + notificationType);
		}  
		ControllerVO(_controller[ controller.getName() ]).commands[notificationType] = null;
		delete ControllerVO(_controller[ controller.getName() ]).commands[notificationType]; 	
  	}      
  	
  	/**
  	 * retrieves a registered command class reference
  	 * @param controller
  	 * @param notificationType
  	 * @return 
  	 * 
  	 */      	
  	public function retrieveCommand(controllerName:String, notificationType:String):Class
	{	
		if(controllerName  == null)
		{
			throw new Error( 'controllerName not given!');	
		}

		if(ControllerVO(_controller[ controllerName ]).commands[notificationType] == null)
		{
			log(controllerName, 'command for notificationtype "' +  notificationType +'" not registered!');	
				 
			return null;
		}
		return ControllerVO(_controller[ controllerName ]).commands[notificationType];
	}
		
	/**
	 * registered the model
	 * @param controller
	 * @param modelName
	 * @param modelClassRef
	 * 
	 */		
	public function registerModel(controller:Controller, modelName:String, modelClassRef:Class):void
  	{ 		
  		if(isModelRegistered(controller.getName(), modelName) )
		{	
			throw new Error('model already registered: ' + modelName);				
  		}
  		
  		var model:Object =  new modelClassRef(modelName, controller);
  		if(model is Model)
  		{
  			ControllerVO(_controller[ controller.getName() ]).models[ modelName ] = model;
			ControllerVO(_controller[ controller.getName() ]).models[ modelName ].registerHandler();
			
			//notify Application that model has registered
			controller.sendNotification(new NoteVO(Controller.MODEL_REGISTER, null, modelName, model) );

  			log(controller.getName(), 'Model registered: ' +  modelName + ' ClassReference: ' + modelClassRef + ' to Controller: ' + controller.getName() );		
  		}
  		else
  		{
  			throw new Error('invalid modelClassRef!');
  		}
  	}
  	
  	/**
  	 * removes a registered Model 
  	 * @param controller
  	 * @param modelName
  	 * 
  	 */      	
  	public function removeModel(controller:Controller, modelName:String):void
  	{
  		if( ControllerVO(_controller[ controller.getName() ]).models[modelName] == null)
		{
        	throw new Error('model not registered: ' + modelName);
		}  
		
		//notify Application before model is removed
		controller.sendNotification( new NoteVO(Controller.MODEL_REMOVE, null, modelName, ControllerVO(_controller[ controller.getName() ]).models[modelName]) );	

		IModel(ControllerVO(_controller[ controller.getName() ]).models[modelName]).removeHandler();
		ControllerVO(_controller[ controller.getName() ]).models[modelName] = null;
		delete ControllerVO(_controller[ controller.getName() ]).models[modelName]; 
		
		log(controller.getName(), 'Model removed: ' +  modelName );
  	}
  	
	/**
	 * retrieves a registered Model 
	 * @param controller
	 * @param modelName
	 * @return 
	 * 
	 */  	
	public function retrieveModel(controller:Controller, modelName:String):IModel
	{	
		if(ControllerVO(_controller[ controller.getName() ]).models[modelName] == null)
		{
			throw new Error('model not registered: ' + modelName);
		}
		return ControllerVO(_controller[ controller.getName() ]).models[modelName];
	}
	
	/**
	 * registers the given view instance
	 * @param controllerName
	 * @param viewName
	 * @param viewInstance
	 * 
	 */		
	public function registerView(controllerName:String, viewName:String, viewInstance:Object):void
	{
		if( isViewRegistered(controllerName, viewName) )
		{
			throw new Error('view already registered: ' + viewName);
   		}
        //map Instance
        ControllerVO(_controller[ controllerName ]).views[ viewName ] = viewInstance;
        //start dependency injection
        if(viewInstance && viewInstance.constructor)
        {
       	 	findInjectionTargets(viewInstance.constructor, CompactMVC.METATAG_COMPACT_WIRE);
        }
      	//notify Application that view has registered
		retrieveController(controllerName).sendNotification(new NoteVO(Controller.VIEW_REGISTER, null, viewName, viewInstance) );
		//log		
		log(controllerName, 'View registered: ' +  viewName + ' ViewInstance: ' + viewInstance );
	}
	
	/**
	 * dependency injection finder
	 * find variables with typedeclaration to registered models & views having set metadata tag to [CompactWire]
	 * @param myClass
	 * @param metadataTagName
	 * 
	 */	
	private function findInjectionTargets(classRef:Class, metadataTagName:String):void
	{
		if(!classRef || !metadataTagName)
			return;
			
		var classRefXML:XML = describeType(classRef);
		var variableNodes:XMLList = classRefXML.factory.variable;
		var accessorNodes:XMLList = classRefXML.factory.accessor;
		var methodNodes:XMLList = classRefXML.factory.method;
		var nodeXML:XML;
		var foundNodes:Array = [];
		 
		//find name in variables
        for each (nodeXML in variableNodes) 
        {
        	if(nodeXML.metadata)
        	{
        		if(String(nodeXML.metadata.((@name==metadataTagName)).@name).length > 0)
        	 		foundNodes.push(nodeXML);
        	}       	
        }
        
        //find name in accessores    
        for each (nodeXML in accessorNodes) 
        {	
        	if(nodeXML.metadata)
        	{
        		if(String(nodeXML.metadata.((@name==metadataTagName)).@name).length > 0)
        	 		foundNodes.push(nodeXML);
        	}
        	
        }
                
        if(!foundNodes)
        	return;
        	    
		//start injection
		setDependencyInjection(foundNodes);
	}
	
	private function setDependencyInjection(xmlNodes:Array):void
	{
		//start injection
		var nodeXML:XML;
        var variableName:String;
        var variableTypeClassName:String;
        var targetClassName:String;
        var viewName:String;
        var viewInstance:Object;
        var controllerVo:ControllerVO;
        var modelInstance:IModel;
        
        for each(nodeXML in xmlNodes)
        {
        	variableName = nodeXML.@name;
        	variableTypeClassName = nodeXML.@type;
        	targetClassName	= nodeXML.@declaredBy;
        	
        	modelInstance = retrieveModelByClassName(variableTypeClassName);
        	if(modelInstance)
        	{
	     		//iterate through the controller list
	        	for each(controllerVo in _controller)
		    	{
		    		//check if model is registered with the given controller, 
		    		//only models and views registered with same controller are allowed for this operation 
		    		//(preventing memory leaks)
		    		if(isModelRegistered(controllerVo.controller.getName(), modelInstance.getName()) )
		    		{
			    		//iterate through the view list (registered views with controller)
			    		for (viewName in controllerVo.views)
			    		{
			    			//retrieve viewInstance
			    			viewInstance = retrieveView(controllerVo.controller.getName(), viewName);
			    			
			    			//find view with wanted ClassName
			    			if(getQualifiedClassName(viewInstance) == targetClassName)
			    			{
			    				//inject model		    				 				
			    				viewInstance[variableName] = modelInstance;
			    				//log		
								log(controllerVo.controller.getName(), '## set Dependency Injection ## -> ApplicationController [' + getQualifiedClassName(controllerVo.controller) + '] targetInstance ['+viewInstance+'] targetType [' + targetClassName + '] variableName [' + variableName + '] variableType [' + variableTypeClassName + ']' );
			    			}
			    		}
			    	}
		    		
		    	}
		    }
        }
	}
	
	public function retrieveModelByClassName(className:String):IModel
	{	
		var controllerVo:ControllerVO;
		var modelName:String;
		var modelInstance:IModel
		
		for each(controllerVo in _controller)
		{
		 	for each (modelInstance in controllerVo.models)
		    {
		    	if(getQualifiedClassName(modelInstance) == className)
		    	{
		    		return modelInstance;
		    	}
		    }
		}
		return null;
	}
	
	
	/**
	 * removes a registered View 
	 * @param controllerName
	 * @param viewName
	 * 
	 */	
	public function removeView(controllerName:String, viewName:String):void
	{
		if( !isViewRegistered(controllerName, viewName) )
		{
			throw new Error( "view not registered!");
		}
		//notify Application before view is removed
		retrieveController(controllerName).sendNotification( new NoteVO(Controller.VIEW_REMOVE, null, viewName, ControllerVO(_controller[ controllerName ]).views[viewName]) );	
		//remove mapped view
		ControllerVO(_controller[ controllerName ]).views[viewName] = null;
		delete ControllerVO(_controller[ controllerName ]).views[viewName];
		//log
		log(controllerName, 'Removed: View registered with ViewName: "' +  viewName + ' to controller: ' + controllerName);	
	}
	
	/**
	 * retrieves the registered view
	 * @param controllerName
	 * @param viewName
	 * @return 
	 * 
	 */		
	public function retrieveView(controllerName:String, viewName:String):Object
	{
		if( !isViewRegistered(controllerName, viewName) )
		{
			throw new Error( "view not registered!");   	
		} 	
		return ControllerVO(_controller[ controllerName ]).views[ viewName ];
	}
	
	/**
	 * resets a controller 
	 * @param controllerName
	 * 
	 */	
	public function cleanup(controllerName:String):void
	{
		var controllerVo:ControllerVO = _controller[controllerName];

		//remove all views
		for (var viewKey:String in controllerVo.views)
		{
			removeView(controllerName, viewKey);
		}
				
		//remove all Models
		for (var modelKey:String in controllerVo.models)
		{
			removeModel(controllerVo.controller, modelKey);
		}
		//remove all Commands
		for (var commandKey:String in controllerVo.commands)
		{
			removeCommand(controllerVo.controller, commandKey);
		}
		//remove Controller
		_controller[controllerName ] = null;
		delete _controller[controllerName ];
		
		//log status
		logStatus(controllerName, controllerVo);
	}
	
	/**
	 * checks whether a controller is present 
	 * @param controllerName
	 * @return 
	 * 
	 */		
	public function isControllerRegistered(controllerName:String):Boolean
	{
		if(_controller[ controllerName ] != null)
		{
			return true;
		}
		return false;
	}
	
	/**
	 * checks whether a model is present  
	 * @param controllerName
	 * @param modelName
	 * @return 
	 * 
	 */		
	public function isModelRegistered(controllerName:String, modelName:String):Boolean
	{
		if( isControllerRegistered(controllerName) )
		{
			if(ControllerVO(_controller[ controllerName ]).models[ modelName ] != null)
			{
				return true;
			}
		}			
		return false;
	}
	
	/**
	 * checks whether a view is present  
	 * @param controllerName
	 * @param viewName
	 * @return 
	 * 
	 */		
	public function isViewRegistered(controllerName:String, viewName:String):Boolean
	{
		if( isControllerRegistered(controllerName) )
		{
			if(ControllerVO(_controller[ controllerName ]).views[viewName] != null)
			{
				return true;
			}
		}
		return false;
	}
	
	/**
	 * retrieves all registered Views of given controller 
	 * @param controllerName
	 * @return 
	 * 
	 */	
	public function retrieveAllRegisteredViews(controllerName:String):Array
	{
		if( isControllerRegistered(controllerName) )
		{
			if(ControllerVO(_controller[ controllerName ]).views != null)
			{
				return ControllerVO(_controller[ controllerName ]).views;
			}
		}
		return null;
	}
	
	
	
	/**
	 * trace log message 
	 * @param controllerName
	 * @param message
	 * 
	 */	
	private function log(controllerName:String, message:String):void
	{
		if(!isControllerRegistered(controllerName))
			return;
			
		switch( ControllerVO(_controller[ controllerName ]).controller.logLevel)
		{
			case CompactMVC.LOGLEVEL_ALL:		
				logStatus(controllerName, ControllerVO(_controller[ controllerName ]));
			case CompactMVC.LOGLEVEL_DEBUG:
				logMessage(ControllerVO(_controller[ controllerName ]).controller, message);
		}
	}
	
	/**
	 * trace log message
	 * @param controller
	 * @param message
	 * 
	 */	
	private function logMessage(controller:Controller, message:String):void
	{	
		var msg:String = currentTime + ' ' + message;	
		if(controller.logFunction != null)
			controller.logFunction(message);
		else
			trace(msg);
	}
	
	/**
	 * trace the controller status 
	 * @param controllerName
	 * @param controllerVO
	 * 
	 */	
	private function logStatus(controllerName:String, controllerVO:ControllerVO):void
	{
		logMessage(controllerVO.controller, '');
 		logMessage(controllerVO.controller, '[ -- DETAILED LOG START -- ]');
		logMessage(controllerVO.controller, '[ -- Controller Name: ' + controllerName + '	[' + getQualifiedClassName(controllerVO.controller)  + ']');
		
		//list commands
		logMessage(controllerVO.controller, '     Commands ------------------------->');
		for (var commandKey:String in controllerVO.commands)
		{
			logMessage(controllerVO.controller, '	Notification Type: '+ commandKey + '	[' + getQualifiedClassName( controllerVO.commands[commandKey]) + ']'); 
		}
		
		//list models
		logMessage(controllerVO.controller, '     Models --------------------------->');
		for (var modelKey:String in controllerVO.models)
		{
			logMessage(controllerVO.controller, '	Model Name: ' + modelKey + '	[' +  getQualifiedClassName( controllerVO.models[modelKey]) + ']' ); 
		}
		
		//list views
		logMessage(controllerVO.controller, '     Views ---------------------------->');
		for (var viewKey:String in controllerVO.views)
		{
			logMessage(controllerVO.controller, '	View Name: ' + viewKey + ' [' + getQualifiedClassName( controllerVO.views[viewKey]) + ']' );
		}
		
		logMessage(controllerVO.controller, ']');		
		logMessage(controllerVO.controller, '[ -- DETAILED LOG STOP -- ]');
		logMessage(controllerVO.controller, '');
	}
	
	/**
	 * Creates a String of valid time value
	 * @return 			String 		current time as a String using valid hours, minutes, seconds and milliseconds
	 */	 
	private function get currentTime ():String
    {
		var date:Date = new Date();
		return "CompactMVC " + fixTime(date.getHours()) + ':' + fixTime(date.getMinutes()) + ':' + fixTime(date.getSeconds()) + '.' + fixTime(date.getMilliseconds());
    }
    
    private function fixTime(value:Number):String
    {
    	return value > 9 ? value.toString() : "0" + value.toString();
    }
}

class SingletonEnforcer {}
